Skip to content

fix: filter functional members from DM room display name heroes#2276

Open
beezly wants to merge 4 commits into
famedly:mainfrom
beezly:fix/dm-hero-filtering
Open

fix: filter functional members from DM room display name heroes#2276
beezly wants to merge 4 commits into
famedly:mainfrom
beezly:fix/dm-hero-filtering

Conversation

@beezly

@beezly beezly commented Feb 26, 2026

Copy link
Copy Markdown

Problem

When a room is marked as a direct message (via m.direct account data) and contains bridge bots or service members listed in the io.element.functional_members state event, those bots were incorrectly included in the room hero list and therefore appeared in the room display name.

For example, if a WhatsApp bridge bot (@whatsappbot:server) is a member of a DM room alongside the real contact, the room would be named something like "Alice and @whatsappbot:server" instead of just "Alice".

Solution

This PR adds filtering of functional members from the hero list when computing the display name and loading hero users for DM rooms. It mirrors the approach used in the matrix-js-sdk's getFunctionalMembers().

Changes

  • Room.functionalMembers getter — reads the service_members list from the io.element.functional_members state event
  • getLocalizedDisplayname() — excludes functional members from the heroes list when directChatMatrixID is set
  • loadHeroUsers() — applies the same filter for consistency

References


Signed-off-by: Andrew Beresford beezly@beez.ly

Summary by CodeRabbit

  • Bug Fixes

    • Direct message rooms now properly exclude service members and bots from display names and hero user lists.
  • Tests

    • Added tests to verify functional member filtering in direct chat display names.

Review Change Stack

@CLAassistant

CLAassistant commented Feb 26, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@beezly

beezly commented Feb 26, 2026

Copy link
Copy Markdown
Author

Closing — not ready for review yet.

@beezly beezly closed this Feb 26, 2026
@beezly beezly reopened this Mar 15, 2026
@beezly beezly force-pushed the fix/dm-hero-filtering branch from 067003c to 45997ba Compare March 15, 2026 13:21
beezly added 2 commits March 17, 2026 10:31
When a room is marked as a DM (via m.direct account data) and contains
bridge bots or service members listed in the io.element.functional_members
state event, those users were incorrectly included in the room display
name and hero list.

This mirrors the JS SDK's getFunctionalMembers() approach: read the
service_members list from the io.element.functional_members state event
and exclude those user IDs from heroes when computing the room name for
a DM room.

Adds:
- Room.functionalMembers getter to read io.element.functional_members
- Filtering in getLocalizedDisplayname() to exclude functional members
  from heroes when directChatMatrixID is set
- Filtering in loadHeroUsers() for the same reason

Fixes: krille-chan/fluffychat#2116
See: https://github.com/element-hq/element-meta/blob/develop/spec/functional_members.md
@beezly beezly force-pushed the fix/dm-hero-filtering branch from 45997ba to a5093c7 Compare March 17, 2026 10:33
@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR adds support for filtering out functional members (bots/bridges) from direct-message rooms. A new functionalMembers property reads the io.element.functional_members state event, then filters these members from DM hero loading and localized display names. Tests validate the filtering behavior in both scenarios.

Changes

DM Functional Member Filtering

Layer / File(s) Summary
Functional members state property
lib/src/room.dart
New functionalMembers getter reads io.element.functional_members state, extracts and returns the service_members list, or an empty list if missing/invalid.
DM room hero and display name filtering
lib/src/room.dart
loadHeroUsers() filters functional members from hero lists for DM rooms. getLocalizedDisplayname() excludes functional members from computed display names for direct chats.
Test coverage
test/room_test.dart
Test verifies functional members are filtered from DM display names when the state event is present; includes negative case confirming bots appear when state is absent.

🎯 2 (Simple) | ⏱️ ~12 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: filtering functional members from DM room display name heroes, which directly matches the core modifications in both room.dart and the test file.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
lib/src/room.dart (2)

210-218: ⚡ Quick win

Redundant variable read for directChatMatrixID.

directChatMatrixID is already read at line 202 and can be reused here instead of re-reading it at line 212.

♻️ Refactor to reuse the variable from line 202
     if (heroes == null) return [];
 
-    // Filter out functional members (bots/bridges) when in a DM room,
-    // so that hero user loading only fetches the real human participant.
-    final directChatMatrixID = this.directChatMatrixID;
-    if (directChatMatrixID != null) {
+    // Filter out functional members (bots/bridges) when in a DM room,
+    // so that hero user loading only fetches the real human participant.
+    if (directChatMatrixID != null) {
       final fMembers = functionalMembers;
       if (fMembers.isNotEmpty) {
         heroes = heroes.where((h) => !fMembers.contains(h)).toList();
       }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/src/room.dart` around lines 210 - 218, The code redundantly re-reads
directChatMatrixID; reuse the previously read directChatMatrixID variable (from
earlier in the function) instead of re-evaluating it here—locate the existing
directChatMatrixID declaration and use that reference in the conditional that
filters out functionalMembers from heroes (keep the functionalMembers and heroes
logic unchanged), removing the extra read to eliminate redundancy.

391-404: ⚡ Quick win

Consider using tryGetList for consistency with codebase patterns.

The current implementation manually checks the type and filters, but the codebase has a tryGetList extension method (see lib/matrix_api_lite/utils/try_get_map_extension.dart) for extracting typed lists from event content.

♻️ Use tryGetList for idiomatic list extraction
   List<String> get functionalMembers {
-    final event = getState('io.element.functional_members');
-    final serviceMembers = event?.content['service_members'];
-    if (serviceMembers is List) {
-      return serviceMembers.whereType<String>().toList();
-    }
-    return [];
+    return getState('io.element.functional_members')
+            ?.content
+            .tryGetList<String>('service_members', TryGet.silent) ??
+        [];
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/src/room.dart` around lines 391 - 404, Replace the manual
type-check/filter in the Room.functionalMembers getter with the project's
tryGetList helper: fetch the state event via
getState('io.element.functional_members'), then call
event?.content.tryGetList('service_members') (or the appropriate extension
import) and return that result (or [] if null) so the extraction is consistent
and idiomatic with other code using tryGetList.
test/room_test.dart (1)

1098-1103: ⚡ Quick win

Strengthen negative test assertion.

The negative case uses isNot('John Doe') to verify the bot appears when io.element.functional_members is absent. This assertion is weak because it would pass for any result that isn't exactly "John Doe" (e.g., empty string, error message).

A stronger assertion would explicitly check that the bot's display name appears alongside the real user.

💪 Stronger assertion
-        // Without functional_members, the bot appears alongside the real user
-        expect(
-          dmRoomNoFilter.getLocalizedDisplayname(),
-          isNot('John Doe'),
-          reason:
-              'Without io.element.functional_members, bot should still appear in room name',
-        );
+        // Without functional_members, the bot appears alongside the real user
+        final displayName = dmRoomNoFilter.getLocalizedDisplayname();
+        expect(
+          displayName.contains('Signal Bridge Bot') || displayName.contains('signal-bot'),
+          true,
+          reason:
+              'Without io.element.functional_members, bot should appear in room name',
+        );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/room_test.dart` around lines 1098 - 1103, The negative assertion using
isNot('John Doe') is too weak; update the test around
dmRoomNoFilter.getLocalizedDisplayname() to explicitly assert that the returned
display name contains both the bot's display name and the real user's name
(e.g., use contains matchers or a regex against getLocalizedDisplayname() to
verify presence of the bot label and "John Doe"), so that the test guarantees
the bot appears alongside the real user rather than merely not equaling "John
Doe".
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@lib/src/room.dart`:
- Around line 210-218: The code redundantly re-reads directChatMatrixID; reuse
the previously read directChatMatrixID variable (from earlier in the function)
instead of re-evaluating it here—locate the existing directChatMatrixID
declaration and use that reference in the conditional that filters out
functionalMembers from heroes (keep the functionalMembers and heroes logic
unchanged), removing the extra read to eliminate redundancy.
- Around line 391-404: Replace the manual type-check/filter in the
Room.functionalMembers getter with the project's tryGetList helper: fetch the
state event via getState('io.element.functional_members'), then call
event?.content.tryGetList('service_members') (or the appropriate extension
import) and return that result (or [] if null) so the extraction is consistent
and idiomatic with other code using tryGetList.

In `@test/room_test.dart`:
- Around line 1098-1103: The negative assertion using isNot('John Doe') is too
weak; update the test around dmRoomNoFilter.getLocalizedDisplayname() to
explicitly assert that the returned display name contains both the bot's display
name and the real user's name (e.g., use contains matchers or a regex against
getLocalizedDisplayname() to verify presence of the bot label and "John Doe"),
so that the test guarantees the bot appears alongside the real user rather than
merely not equaling "John Doe".

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4b1d6d7f-d57c-420e-b64e-f80ef351ebbe

📥 Commits

Reviewing files that changed from the base of the PR and between 8bf30ac and a3fab1b.

📒 Files selected for processing (2)
  • lib/src/room.dart
  • test/room_test.dart

@c00

c00 commented Jul 1, 2026

Copy link
Copy Markdown

I would love to see this PR merged. I am waiting for this fix.

@cursor

cursor Bot commented Jul 3, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Display-name and hero-list logic only; filtering is scoped to DM rooms and gated on the optional functional-members state event.

Overview
Fixes bridged DM rooms showing bridge bots in the chat title when heroes include both the human contact and a service member.

Adds Room.functionalMembers, which reads service_members from the io.element.functional_members room state (aligned with matrix-js-sdk). For rooms with a directChatMatrixID, those IDs are stripped from the hero list in getLocalizedDisplayname() and loadHeroUsers(), so names like "John Doe" replace "John Doe, Signal Bridge Bot". Non-DM rooms and DMs without that state event are unchanged.

Tests cover DM naming with and without the functional-members state.

Reviewed by Cursor Bugbot for commit 2f053eb. Bugbot is set up for automated code reviews on this repo. Configure here.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is ON, but it could not run because the branch was deleted or merged before autofix could start.

Reviewed by Cursor Bugbot for commit 2f053eb. Configure here.

Comment thread lib/src/room.dart
hero.isNotEmpty &&
hero != client.userID &&
(directChatMatrixID == null ||
!functionalMembers.contains(hero)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blank DM title after filter

Medium Severity

The new functional member filtering in DMs can result in empty hero lists for getLocalizedDisplayname and loadHeroUsers. This occurs because the directChatMatrixID fallback is only applied if m.heroes is empty before filtering, not after, leading to blank display names or no hero users when m.heroes initially contains only functional members.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 2f053eb. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants